﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;

namespace DependencyAnalysis.DataModel
{
    public static class DescriptorBuilder
    {
        public const string LinqIdentifier = "+<>c__DisplayClass";
        private const string GenericIdentifier = "`";

        private static readonly object LockCache = new object();

        private static readonly Dictionary<string, ModuleDescriptor> AssemblyCache = new Dictionary<string, ModuleDescriptor>();
        private static readonly Dictionary<string, MemberDescriptor> MemberCache = new Dictionary<string, MemberDescriptor>();
        private static readonly Dictionary<string, TypeDescriptor> TypeCache = new Dictionary<string, TypeDescriptor>();
        private static readonly Dictionary<string, SubmoduleDescriptor> NamespaceCache = new Dictionary<string, SubmoduleDescriptor>();
        private static readonly Dictionary<string, DependencyDescriptor> DependencyCache = new Dictionary<string, DependencyDescriptor>();

        private static readonly byte[] PublicKeyTokenClr = { 0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89 };
        private static readonly byte[] PublicKeyTokenFx = { 0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a };
        private static readonly byte[] PublicKeyTokenWpf = { 0x31, 0xbf, 0x38, 0x56, 0xad, 0x36, 0x4e, 0x35 };

        /// <summary>
        /// Empty the caches containing found descriptors for modules, submodules,
        /// types, members and dependencies. Before analyzing of a project starts the caches
        /// are cleared.
        /// </summary>
        public static void FlushCache()
        {
            lock (LockCache)
            {
                DependencyCache.Clear();
                AssemblyCache.Clear();
                MemberCache.Clear();
                TypeCache.Clear();
                NamespaceCache.Clear();
            }
        }

        /// <summary>
        /// Create a descriptor for the requested assembly. Submodules are not filled nor are any properties of 
        /// ModuleDescriptor set that require analyzing the target. When the descriptor is already cached then
        /// that version of returned.
        /// </summary>
        /// <param name="module">an assembly</param>
        /// <returns></returns>
        public static ModuleDescriptor BuildModuleDescriptor(_Assembly module)
        {
            var assemblyIdentifier = CreateAssemblyIdentifier(module);

            lock (LockCache)
            {
                if (AssemblyCache.ContainsKey(assemblyIdentifier)) return AssemblyCache[assemblyIdentifier];
                var keyToken = module.GetName().GetPublicKeyToken();
                bool isPartOfFramework = PublicKeyTokensAreEqual(PublicKeyTokenClr, keyToken) || PublicKeyTokensAreEqual(PublicKeyTokenFx, keyToken) || PublicKeyTokensAreEqual(PublicKeyTokenWpf, keyToken);
                var moduleDescriptor = new ModuleDescriptor
                                           {
                                               Identifier = assemblyIdentifier,
                                               FullName = module.GetName().Name,
                                               IsPartOfFramework = isPartOfFramework
                                           };
                AssemblyCache[assemblyIdentifier] = moduleDescriptor;
                return moduleDescriptor;
            }
        }

        private static bool PublicKeyTokensAreEqual(byte[] token1, byte[] token2)
        {
            if (token1 == token2) return true;
            if (token1 == null || token2 == null || token1.Length != token2.Length)
                return false;

            return !token1.Where((t, i) => t != token2[i]).Any();
        }

        private static string CreateAssemblyIdentifier(_Assembly module)
        {
            var name = module.GetName();
            return name.Name + " @ " + name.Version;
        }

        private static NodeVisibility DetermineVisibilityForMember(MemberInfo member)
        {
            var methodInfo = member as _MethodInfo;
            if (methodInfo != null) return DetermineVisibilityForMember(methodInfo);
            var type = member as _Type;
            if (type != null) return DetermineVisibilityForMember(type);
            var fieldInfo = member as _FieldInfo;
            if (fieldInfo != null) return DetermineVisibilityForMember(fieldInfo);
            return NodeVisibility.Ignore;
        }

        private static NodeVisibility DetermineVisibilityForMember(_MethodInfo member)
        {
            if (member.IsPublic) return NodeVisibility.Public;
            if (member.IsPrivate) return NodeVisibility.Private;
            if (member.IsFamilyAndAssembly) return NodeVisibility.Protected | NodeVisibility.Internal;
            if (member.IsFamily) return NodeVisibility.Internal;
            return NodeVisibility.Protected;
        }

        private static NodeVisibility DetermineVisibilityForMember(_Type member)
        {
            if (member.IsPublic) return NodeVisibility.Public;
            if (member.IsNestedPrivate) return NodeVisibility.Private;
            if (member.IsNotPublic) return NodeVisibility.Internal;
            return NodeVisibility.Ignore;
        }

        private static NodeVisibility DetermineVisibilityForMember(_FieldInfo member)
        {
            try
            {
                if (typeof(MulticastDelegate).IsAssignableFrom(member.FieldType))
                {
                    if (member != null && member.DeclaringType != null)
                    {
                        var eventInfo = member.DeclaringType.GetEvent(member.Name);
                        if (eventInfo != null)
                        {
                            return DetermineVisibilityForMember((_MethodInfo)eventInfo.GetAddMethod());
                        }
                    }
                }
            }
            catch (TypeLoadException)
            {
            }

            if (member.IsPublic) return NodeVisibility.Public;
            if (member.IsPrivate) return NodeVisibility.Private;
            if (member.IsFamilyAndAssembly) return NodeVisibility.Protected | NodeVisibility.Internal;
            if (member.IsFamily) return NodeVisibility.Internal;
            return NodeVisibility.Protected;
        }

        public static string ExtractNamespaceFromType(Type type)
        {
            try
            {
                return type.Namespace;
            }
            catch (FileLoadException)
            {
                var fullName = type.FullName;
                var lastDot = fullName.LastIndexOf(".", StringComparison.InvariantCulture);
                return fullName.Substring(0, lastDot);
            }
        }

        /// <summary>
        /// Create a descriptor for a namespace in a module. types are not filled nor are any properties of 
        /// SubmoduleDescriptor set that require analyzing the target. When the descriptor is already cached then
        /// that version of returned.
        /// </summary>
        /// <param name="moduleDescriptor"></param>
        /// <param name="typeNamespace"></param>
        /// <returns></returns>
        public static SubmoduleDescriptor BuildSubmoduleDescriptor(ModuleDescriptor moduleDescriptor,
                                                                   string typeNamespace)
        {
            if (typeNamespace == null)
            {
                typeNamespace = string.Empty;
            }
            typeNamespace = typeNamespace.Replace('\\', '.');
            typeNamespace = typeNamespace.Replace('/', '.');
            var namespaceIdentifier = moduleDescriptor.Identifier + " /submodule " + typeNamespace;
            lock (LockCache)
            {
                if (NamespaceCache.ContainsKey(namespaceIdentifier)) return NamespaceCache[namespaceIdentifier];
                string name = typeNamespace;
                if (moduleDescriptor.FullName != null &&
                    typeNamespace.StartsWith(moduleDescriptor.FullName))
                {
                    name = typeNamespace.Substring(moduleDescriptor.FullName.Length);
                    if (String.IsNullOrEmpty(name))
                    {
                        name = " / ";
                    }
                }
                var submoduleDescriptor = new SubmoduleDescriptor
                    {
                        Module = moduleDescriptor,
                        FullName = typeNamespace,
                        Name = name,
                        Identifier = namespaceIdentifier
                    };
                NamespaceCache[namespaceIdentifier] = submoduleDescriptor;
                return submoduleDescriptor;
            }
        }

        internal static TypeDescriptor BuildTypeDescriptor(Type type, SubmoduleDescriptor submoduleDescriptor)
        {
            if (type.IsGenericType)
            {
                type = type.GetGenericTypeDefinition();
            }
            var module = type.Assembly;
            string fullName = ExtractTypeName(type.FullName);
            string name = ExtractTypeName(type.Name);
            var typeIdentifier = CreateAssemblyIdentifier(module) + " /type " + fullName;
            bool isLinq = !string.IsNullOrEmpty(type.FullName) && type.FullName.Contains(LinqIdentifier);
            lock (LockCache)
            {
                TypeDescriptor typeDescriptor;
                if (TypeCache.ContainsKey(typeIdentifier))
                {
                    typeDescriptor = TypeCache[typeIdentifier];
                }
                else
                {
                    typeDescriptor = new TypeDescriptor
                    {
                        FullName = fullName,
                        Identifier = typeIdentifier,
                        Submodule = submoduleDescriptor,
                    };
                    TypeCache[typeIdentifier] = typeDescriptor;
                }
                if (!isLinq)
                {
                    typeDescriptor.IsEnum = type.IsEnum;
                    typeDescriptor.Type = type;
                    typeDescriptor.Name = name;
                    typeDescriptor.Visibility = DetermineVisibilityForMember((_Type)type);
                    typeDescriptor.AbstractionType = DetermineAbstractionType(type);
                    if (type.Name.StartsWith("<"))
                    {
                        typeDescriptor.IsGenerated = true;
                    }
                }
                return typeDescriptor;
            }
        }

        private static string ExtractTypeName(string name)
        {
            if (name == null) return String.Empty;
            var indexOf = name.IndexOf(LinqIdentifier);
            if (indexOf > -1)
            {
                name = name.Substring(0, indexOf);
            }
            indexOf = name.IndexOf(GenericIdentifier);
            if (indexOf > -1)
            {
                name = name.Substring(0, indexOf);
            }
            return name;
        }

        /// <summary>
        /// Create a descriptor for a type. Members are not filled nor are any properties of 
        /// TypeDescriptor set that require analyzing the target. When the descriptor is already cached then
        /// that version of returned.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static TypeDescriptor BuildTypeDescriptor(Type type)
        {
            if (type.IsGenericType)
            {
                type = type.GetGenericTypeDefinition();
            }
            string fullName = ExtractTypeName(type.FullName);
            string name = ExtractTypeName(type.Name);
            var identifier = CreateAssemblyIdentifier(type.Assembly) + " /type " + fullName;
            lock (LockCache)
            {
                if (TypeCache.ContainsKey(identifier))
                {
                    return TypeCache[identifier];
                }
            }
            var typeDescriptor = new TypeDescriptor
            {
                IsEnum = type.IsEnum,
                FullName = fullName,
                Identifier = identifier,
                Name = name,
                Type = type,
                Visibility = NodeVisibility.Ignore,
                AbstractionType = DetermineAbstractionType(type)
            };
            return typeDescriptor;
        }

        private static AbstractionType DetermineAbstractionType(Type type)
        {
            if (type.IsAbstract && type.IsSealed)
            {
                return AbstractionType.Static;
            }
            return type.IsInterface ? AbstractionType.Interface : type.IsAbstract ? AbstractionType.Abstract : AbstractionType.Concrete;
        }

        internal static DependencyDescriptor BuildDependencyDescriptor(MemberDescriptor source, MemberDescriptor target,
            ReferenceType referenceType)
        {
            var identifier = referenceType + " @ " + source.Identifier + " -> " + target.Identifier;
            lock (DependencyCache)
            {
                if (DependencyCache.ContainsKey(identifier))
                {
                    return DependencyCache[identifier];
                }
            }

            var dependencyDescriptor = new DependencyDescriptor
            {
                Source = source,
                ReferenceType = referenceType,
                Target = target,
                Identifier = identifier
            };
            lock (DependencyCache)
            {
                if (!DependencyCache.ContainsKey(identifier))
                {
                    return DependencyCache[identifier] = dependencyDescriptor;
                }
            }
            return dependencyDescriptor;
        }

        internal static DependencyDescriptor BuildDependencyDescriptorForMember(MemberInfo targetMember, MemberDescriptor memberDescriptor, ReferenceType referenceType)
        {
            var targetMemberDescriptor = BuildMemberDescriptor(targetMember);
            var identifier = referenceType + " @ " + memberDescriptor.Identifier + " -> " + targetMemberDescriptor.Identifier;
            lock (DependencyCache)
            {
                if (DependencyCache.ContainsKey(identifier))
                {
                    return DependencyCache[identifier];
                }
            }
            var targetType = targetMember.ReflectedType;
            var targetNamespace = ExtractNamespaceFromType(targetType);
            var targetAssembly = targetType.Assembly;
            var targetAssemblyDescriptor = BuildModuleDescriptor(targetAssembly);
            var targetNamespaceDescriptor = BuildSubmoduleDescriptor(targetAssemblyDescriptor, targetNamespace);
            var targetTypeDescriptor = BuildTypeDescriptor(targetType, targetNamespaceDescriptor);
            targetMemberDescriptor.ImplementingType = targetTypeDescriptor;

            var dependencyDescriptor = new DependencyDescriptor
            {
                Source = memberDescriptor,
                ReferenceType = referenceType,
                Target = targetMemberDescriptor,
                Identifier = identifier
            };
            lock (DependencyCache)
            {
                if (!DependencyCache.ContainsKey(identifier))
                {
                    return DependencyCache[identifier] = dependencyDescriptor;
                }
            }
            return dependencyDescriptor;
        }

        internal static DependencyDescriptor BuildDependencyDescriptorForTypeReference(Type targetType, MemberDescriptor memberDescriptor, ReferenceType referenceType)
        {
            var identifier = referenceType + " @ " + memberDescriptor.Identifier + " -> " + (targetType == null ? "?" : targetType.FullName);
            lock (DependencyCache)
            {
                if (DependencyCache.ContainsKey(identifier))
                {
                    return DependencyCache[identifier];
                }
            }
            DependencyDescriptor dependencyDescriptor;
            if (targetType == null)
            {
                dependencyDescriptor = new DependencyDescriptor
                {
                    Source = memberDescriptor,
                    ReferenceType = referenceType,
                    Target = new MemberDescriptor { MemberType = MemberType.TypeReference, FullName = "?", Name = "?", Identifier = "?", ImplementingType = new TypeDescriptor { Identifier = "?", Name = "?", FullName = "?", Submodule = new SubmoduleDescriptor { Identifier = "?", Name = "?", FullName = "?", Module = new ModuleDescriptor { FullName = "?", Identifier = "?" } } } },
                    Identifier =
                        identifier
                };
            }
            else
            {
                var targetNamespace = ExtractNamespaceFromType(targetType);
                var targetAssembly = targetType.Assembly;
                var targetAssemblyDescriptor = BuildModuleDescriptor(targetAssembly);
                var targetNamespaceDescriptor = BuildSubmoduleDescriptor(targetAssemblyDescriptor, targetNamespace);
                var targetTypeDescriptor = BuildTypeDescriptor(targetType, targetNamespaceDescriptor);
                MemberDescriptor targetMemberDescriptor = BuildMemberDescriptor(targetType, targetTypeDescriptor, true);
                dependencyDescriptor = new DependencyDescriptor
                {
                    Source = memberDescriptor,
                    ReferenceType = referenceType,
                    Target = targetMemberDescriptor,
                    Identifier =
                        identifier
                };
            }
            lock (DependencyCache)
            {
                if (!DependencyCache.ContainsKey(identifier))
                {
                    return DependencyCache[identifier] = dependencyDescriptor;
                }
            }
            return dependencyDescriptor;
        }

        /// <summary>
        /// Creates a descriptor for the given member. When the member is already cached then that version
        /// is returned. No properties are filled that require analysis.
        /// </summary>
        /// <param name="member"></param>
        /// <returns></returns>
        public static MemberDescriptor BuildMemberDescriptor(MemberInfo member)
        {
            return BuildMemberDescriptor(member, BuildTypeDescriptor(member.ReflectedType), false);
        }

        internal static MemberDescriptor BuildMemberDescriptor(MemberInfo member, TypeDescriptor typeDescriptor, bool justReference)
        {
            var toType = member as Type;
            string innerTypeName = ExtractInnerTypeName(member, toType);

            string fullname;
            Assembly assembly;
            string cacheIdentifier;
            if (toType == null)
            {
                try
                {
                    fullname = typeDescriptor.FullName + innerTypeName + ".§" + member;
                }
                catch (TypeLoadException)
                {
                    fullname = typeDescriptor.FullName + innerTypeName + ".§" + member.Name;
                }
                assembly = member.ReflectedType.Assembly;
                cacheIdentifier = typeDescriptor.FullName + innerTypeName + ".§" + member.Name;
            }
            else
            {
                fullname = toType.FullName;
                assembly = toType.Assembly;
                cacheIdentifier = toType.FullName;
            }
            cacheIdentifier += +member.MetadataToken;
            var memberIdentifier = (justReference ? fullname + " /reference " : assembly.Location + " /member ") + fullname;
            var fieldInfo = member as FieldInfo;
            var methodInfo = member as MethodInfo;
            lock (LockCache)
            {
                if (MemberCache.ContainsKey(cacheIdentifier)) return MemberCache[cacheIdentifier];
                var name = string.IsNullOrEmpty(innerTypeName) ? member.Name : innerTypeName + ".§" + member.Name;
                MemberDescriptor memberDescriptor = new MemberDescriptor
                                                        {
                                                            ImplementingType = typeDescriptor,
                                                            FullName =
                                                                justReference ? typeDescriptor.FullName : fullname,
                                                            Identifier = memberIdentifier,
                                                            Visibility = DetermineVisibilityForMember(member),
                                                            MemberType = justReference ? MemberType.TypeReference : (fieldInfo != null ? MemberType.Field : MemberType.Method),
                                                            Name = justReference ? " / " : name,
                                                            IsAbstract = member.MemberType == MemberTypes.Method && ((MethodBase)member).IsAbstract
                                                        };
                if (fieldInfo != null)
                {
                    try
                    {
                        memberDescriptor.ReturningType = BuildTypeDescriptor(fieldInfo.FieldType);
                    }
                    catch (TypeLoadException)
                    {
                    }
                }
                else if (methodInfo != null)
                {
                    try
                    {
                        memberDescriptor.ReturningType = BuildTypeDescriptor(methodInfo.ReturnType);
                    }
                    catch (TypeLoadException)
                    {
                    }
                    try
                    {
                        var parameters = methodInfo.GetParameters();
                        memberDescriptor.ParameterCount = parameters.Count();
                    }
                    catch (TypeLoadException)
                    {
                    }

                }

                MemberCache[cacheIdentifier] = memberDescriptor;
                return memberDescriptor;
            }
        }

        private static string ExtractInnerTypeName(MemberInfo member, Type toType)
        {
            string innerTypeName = string.Empty;
            if (toType == null)
            {
                var name = member.ReflectedType.Name;
                if (name.StartsWith("<>"))
                {
                    innerTypeName = "+" + name;
                }
            }
            return innerTypeName;
        }

        public static MemberDescriptor BuildXamlMemberDescriptor(TypeDescriptor xamlTypeDescriptor, string name)
        {
            var isTypeMember = String.IsNullOrEmpty(name);
            var identifier = xamlTypeDescriptor.Identifier + (isTypeMember ? string.Empty : (".$" + name));
            lock (LockCache)
            {
                if (MemberCache.ContainsKey(identifier)) return MemberCache[identifier];
                var memberDescriptor = new MemberDescriptor
                {
                    Name = isTypeMember ? xamlTypeDescriptor.Name : name,
                    ImplementingType = xamlTypeDescriptor,
                    Identifier = identifier,
                    FullName = xamlTypeDescriptor.FullName + (isTypeMember ? string.Empty : (".$" + name)),
                    MemberType = isTypeMember ? MemberType.TypeReference : MemberType.Field,
                };
                MemberCache[identifier] = memberDescriptor;
                return memberDescriptor;
            }
        }

        public static TypeDescriptor BuildXamlTypeDescriptor(ModuleDescriptor module, string fileName)
        {
            var lastIndexOf = fileName.LastIndexOf('/');
            var typeName = fileName.Substring(lastIndexOf + 1).Replace(".baml", ".xaml");
            string path = module.Name;
            if (lastIndexOf > -1)
            {
                path += "." + fileName.Substring(0, lastIndexOf);
            }
            var submodule = BuildSubmoduleDescriptor(module, path);
            var identifier = submodule.Module.Identifier + " /type " + path + "." + typeName;
            lock (LockCache)
            {
                if (TypeCache.ContainsKey(identifier)) return TypeCache[identifier];
                var typeDescriptor = new TypeDescriptor
                {
                    Name = "<XAML>" + typeName,
                    Submodule = submodule,
                    Identifier = identifier,
                    FullName = fileName,
                    AbstractionType = AbstractionType.Descriptive
                };
                TypeCache[identifier] = typeDescriptor;
                return typeDescriptor;
            }
        }
    }
}
